package ca.scotthyndman.as2haxe.dom;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import ca.scotthyndman.as2haxe.dom.visitor.IVisitor;

/**
 * Represents a package.
 */
public class ASPackage extends ASTNode {

	/**
	 * The "name" structural property of this node type.
	 */
	public static final ASChildPropertyDescriptor NAME_PROPERTY = new ASChildPropertyDescriptor(
			ASPackage.class, "name", ASName.class, true);

	/**
	 * The "packages" structural property of this node type
	 */
	public static final ASChildListPropertyDescriptor PACKAGES_PROPERTY = new ASChildListPropertyDescriptor(
			ASPackage.class, "packages", ASPackage.class);

	/**
	 * The "packages" structural property of this node type
	 */
	public static final ASChildListPropertyDescriptor TYPES_PROPERTY = new ASChildListPropertyDescriptor(
			ASPackage.class, "types", ASTypeDeclaration.class);

	/**
	 * A list of property descriptors (element type:
	 * {@link ASPropertyDescriptor}).
	 */
	private static final List<ASPropertyDescriptor> PROPERTY_DESCRIPTORS;

	static {
		List<Object> propertyList = new ArrayList<Object>(4);
		createPropertyList(ASPackage.class, propertyList);
		addProperty(NAME_PROPERTY, propertyList);
		addProperty(PACKAGES_PROPERTY, propertyList);
		addProperty(TYPES_PROPERTY, propertyList);
		PROPERTY_DESCRIPTORS = reapPropertyList(propertyList);
	}

	/**
	 * Returns a list of structural property descriptors for this node type.
	 * Clients must not modify the result.
	 * 
	 * @return a list of property descriptors (element type:
	 *         {@link ASPropertyDescriptor})
	 */
	public static List<ASPropertyDescriptor> propertyDescriptors() {
		return PROPERTY_DESCRIPTORS;
	}

	//
	// ======== MEMBERS
	//

	/** A mapping of all packages */
	private static final Map<String, ASPackage> allPackages = new HashMap<String, ASPackage>();

	/** The root package */
	private static ASPackage rootPackage;

	/** Child packages map */
	private Map<String, ASPackage> packages = new HashMap<String, ASPackage>();

	/** Child types map */
	private Map<String, ASTypeDeclaration> types = new HashMap<String, ASTypeDeclaration>();

	/** Child packages list */
	private NodeList packagesList = new NodeList(PACKAGES_PROPERTY);

	/** Child types list */
	private NodeList typesList = new NodeList(TYPES_PROPERTY);

	/** Package name */
	private ASName name;

	//
	// ======== CONSTRUCTION
	//

	public ASPackage(AST ast, ASName name) {
		super(ast);
		setName(name);

		if (name != null) {
			ast.addPackage(this);
		}
	}

	//
	// ======== GETTERS / SETTERS
	//

	/**
	 * Sets the name to <code>name</code>. This method throws an excpetion if
	 * <code>name</code> is <code>null</code>.
	 * 
	 * @param name
	 *            the new name of the package
	 */
	public void setName(ASName name) {
		if (name == null) {
			throw new IllegalArgumentException();
		}

		ASTNode oldChild = this.name;
		preReplaceChild(oldChild, name, NAME_PROPERTY);

		//
		// Remove this package from the all packages map (if we're already in
		// there)
		//
		if (this.name != null) {
			allPackages.remove(this.name.toString());
		}

		this.name = name;

		//
		// Add this package to the all packages map with its new name
		//
		allPackages.put(name.toString(), this);
		postReplaceChild(oldChild, name, NAME_PROPERTY);
	}

	/**
	 * Returns the name of the package.
	 * 
	 * @return the name
	 */
	public ASName getName() {
		return name;
	}

	/**
	 * Returns the child packages of this package
	 * 
	 * @return The child packages
	 */
	public NodeList getPackages() {
		return packagesList;
	}

	/**
	 * Returns the child types of this package
	 * 
	 * @return The child types
	 */
	public NodeList getTypes() {
		return typesList;
	}

	/**
	 * Returns the map of package names (string of simple name) to packages.
	 * 
	 * @return The map
	 */
	public Map<String, ASPackage> getPackagesMap() {
		return packages;
	}

	/**
	 * Returns the map of type names (string of simple name) to types
	 * declarations.
	 * 
	 * @return The type map
	 */
	public Map<String, ASTypeDeclaration> getTypesMap() {
		return types;
	}

	//
	// ======== CHILDREN MODIFICATION
	//

	/**
	 * Returns whether this package (and all its subpackages) are empty of
	 * types.
	 * 
	 * @return <code>true</code> if there are no classes in this package or
	 *         any of its subpackages
	 */
	public boolean isEmpty() {
		if (!getTypesMap().isEmpty()) {
			return false;
		}

		for (ASPackage pkg : getPackagesMap().values()) {
			if (!pkg.isEmpty()) {
				return false;
			}
		}

		return true;
	}

	//
	// ======== EVENT HANDLERS
	//

	@Override
	protected void postAddListChild(ASTNode child,
			ASChildListPropertyDescriptor property) {
		super.postAddListChild(child, property);

		if (property != PACKAGES_PROPERTY) {
			return;
		}

		//
		// Add the new child package to the package map
		//
		ASPackage pkg = (ASPackage) child;
		packages.put(pkg.getName().toString(), pkg);
	}

	@Override
	protected void postRemoveListChild(ASTNode child,
			ASChildListPropertyDescriptor property) {
		super.postRemoveListChild(child, property);

		if (property != PACKAGES_PROPERTY) {
			return;
		}

		ASPackage pkg = (ASPackage) child;
		packages.remove(pkg.getName().toString());
	}

	@Override
	protected void postReplaceListChild(ASTNode oldChild, ASTNode newChild,
			ASChildListPropertyDescriptor property) {
		super.postReplaceListChild(oldChild, newChild, property);

		if (property != PACKAGES_PROPERTY) {
			return;
		}

		ASPackage oldChildPkg = (ASPackage) oldChild;
		packages.remove(oldChildPkg.getName().toString());

		ASPackage newChildPkg = (ASPackage) newChild;
		packages.put(newChildPkg.getName().toString(), newChildPkg);
	}

	//
	// ======== INTERNAL GETTER SETTER
	//

	/*
	 * (omit javadoc for this method) Method declared on ASTNode.
	 */
	final ASTNode internalGetSetChildProperty(
			ASChildPropertyDescriptor property, boolean get, ASTNode child) {
		if (property == NAME_PROPERTY) {
			if (get) {
				return getName();
			} else {
				setName((ASQualifiedName) child);
				return null;
			}
		}

		// allow default implementation to flag the error
		return super.internalGetSetChildProperty(property, get, child);
	}

	/*
	 * Internal
	 */
	final List<ASTNode> internalGetChildListProperty(
			ASChildListPropertyDescriptor property) {
		if (property == TYPES_PROPERTY) {
			return getTypes();
		}
		if (property == PACKAGES_PROPERTY) {
			return getPackages();
		}

		// allow default implementation to flag the error
		return super.internalGetChildListProperty(property);
	}

	// ====== ROOT PACKAGE

	/**
	 * Returns the root package.
	 */
	public static ASPackage getRootPackage() {
		return rootPackage;
	}

	// ====== HELPER METHODS

	/**
	 * Gets the package named <code>name</code>, or <code>null</code> if
	 * not found.
	 */
	public static ASPackage getPackage(String name) {
		if (name == null) {
			return getRootPackage();
		}
		return allPackages.get(name);
	}

	/**
	 * Gets a package if one exists with the name <code>name</code>, or
	 * creates one and returns it if it doesn't. If the package's parent
	 * packages don't exist, they are created as well.
	 * 
	 * @param name
	 * @return
	 */
	public static ASPackage getOrCreatePackage(ASName name) {
		//
		// Root
		//
		if (name == null || name.toString().length() == 0) {
			return getRootPackage();
		}

		//
		// Already generated
		//
		if (allPackages.containsKey(name.toString())) {
			return allPackages.get(name.toString());
		}

		//
		// Build the package
		//
		ASPackage ret = new ASPackage(rootPackage.getAST(), name);
		ASName quali = name.isQualifiedName() ? ((ASQualifiedName) name)
				.getQualifier() : null;
		ASPackage childPkg = ret;

		// 
		// Build parent packages if necessary
		//
		while (quali != null) {
			if (allPackages.containsKey(quali.toString())) {
				ASPackage qPkg = getOrCreatePackage(quali);
				qPkg.getPackages().add(childPkg);
				break;
			}

			ASPackage parentPkg = new ASPackage(rootPackage.getAST(), quali);
			parentPkg.getPackages().add(childPkg);
			childPkg = parentPkg;

			quali = quali.isQualifiedName() ? ((ASQualifiedName) quali)
					.getQualifier() : null;
		}

		//
		// Add top-level packages to the root package
		//
		if (quali == null) {
			rootPackage.getPackages().add(childPkg);
		}

		return ret;
	}

	public static final void initWithAST(AST ast) {
		rootPackage = new ASPackage(ast, new ASSimpleName(ast, ""));
	}

	// ====== TO STRING AND VISITOR

	@Override
	public String toString() {
		StringBuffer buf = new StringBuffer();
		if (getName() != null) {
			buf.append("p: ").append(getName().toString()).append('\n');
		}
		for (ASPackage p : getPackagesMap().values()) {
			buf.append(p.toString()).append('\n');
		}
		for (ASTypeDeclaration t : getTypesMap().values()) {
			buf.append(t.toString()).append('\n');
		}

		return buf.toString();
	}

	public void accept(IVisitor as2Visitor, boolean recursive) {
		recursive = as2Visitor.visit(this) && recursive;

		if (recursive) {
			Collection<ASPackage> packages = new HashSet<ASPackage>(
					getPackagesMap().values());
			for (ASPackage p : packages) {
				p.accept(as2Visitor, recursive);
			}

			for (ASTypeDeclaration t : getTypesMap().values()) {
				t.accept(as2Visitor, recursive);
			}
		}
	}

}
